#!/usr/bin/env python

import re, sys, getopt, MySQLdb, subprocess

__version__ = "0.0.0"
DEBUG = False

#
# S U B S
#

def usage():
    print sys.argv[0] + " version " + __version__
    print
    print "Usage: depgraph.py [-T type] [-o file, --output=file]"
    print
    print "  -T type   - The type of file desired for the graph (png, svg, ps...)"
    print "  -d dist   - The name of the distribution to analyse"
    print "  -p pkg    - The name of the package to analyse"
    print "  -o file   - The name of the output file"
    print "  -v        - Show this help message"
    print "  -h        - Show this help message"


def debug(msg):
    if DEBUG:
        print >>sys.stderr, msg


def sanitize(word):
    """remove non-alpha-numerical from identifiers"""
    word = re.sub("^\d", "", word) # remove any leading number
    word = remove_parenthesis(word)
    word = re.sub("\W", "", word) # remove special chars
    return word


def remove_parenthesis(word):
    """(...) => nothing!"""
    word = re.sub("\(.+\)", "", word)
    return word


def get_pkg_deps(pkg_id):
    """get package dependencies (only IDs)"""
    debug("getting deps for package " + str(pkg_id) + "...")
    curs.execute("select dependency from PACKAGE_DEPENDENCIES where pid = " + str(pkg_id) + " and type = 'requires'")
    results = curs.fetchall()
    # let's see if all dependencies have a 'provides' equivalent
    pkgs_provide = set([])
    for dep in results:
        if re.match("rpmlib", dep["dependency"]) is not None:
            # ignore any dependency starting with rpmlib
            debug("ignoring " + dep["dependency"])
            continue
        curs.execute("select pid from PACKAGE_DEPENDENCIES where dependency = '" + dep["dependency"] + "' and type = 'provides'")
        rslt = curs.fetchone()
        if rslt is None:
            # suppose the dependency is a file
            pid = get_pkg_of_file(dep["dependency"])
            if pid is not None:
                pkgs_provide.add(pid)
                debug("found " + str(pid) + " using get_pkg_of_file()")
            else:
                # try again but remove the brackets in the dependency
                curs.execute("select pid from PACKAGE_DEPENDENCIES where dependency = '" + remove_parenthesis(dep["dependency"]) + "' and type = 'provides'")
                rslt2 = curs.fetchone()
                if rslt2 is None:
                    # we tried everything we could
                    # write detailed error message but don't exit
                    print >> sys.stderr, "ERROR: " + get_pkg_name(pkg_id) + \
                    " requires " + dep["dependency"] + " but no package provides it"
                else:
                    pkgs_provide.add(rslt2["pid"])
                    debug("found " + str(rslt2["pid"]) + " using dirty pkg provides")
        else:
            pkgs_provide.add(rslt["pid"])
            debug("found " + str(rslt["pid"]) + " using pkg provides")
    return pkgs_provide


def get_pkg_name(pid):
    """get the name of a package from its ID"""
    curs.execute("select project from PACKAGES where id = %s", [pid,])
    rslt = curs.fetchone()
    if rslt is None:
        print >> sys.stderr, "ERROR: no package has ID " + pid
        exit(1)
    else:
        debug("get_pkg_name: ID " + str(pid) + " => " + rslt["project"])
        return rslt["project"]


def get_pkg_id(pkg):
    """get the id of a package"""
    curs.execute("select id from PACKAGES where project = '" + pkg + "'")
    result = curs.fetchone()
    if result is None:
        print >> sys.stderr, "ERROR: cannot find the id of package '" + pkg +"'"
        exit(1)
    debug("get_pkg_id: " + pkg + " => " + str(result["id"]))
    return result["id"]


def get_pkg_of_file(file_name):
    """search to which package belongs a given file"""
    curs.execute("select pid from PACKAGE_FILES where filepath = '" + file_name + "'")
    result1 = curs.fetchone()
    if result1 is None:
        return None
    else:
        return result1["pid"]


def get_pkgs_of_dist(name):
    curs.execute("select pid from DISTRIBUTION_PACKAGES where distribution = '"+name+"'")
    results = curs.fetchall()
    pkgs_list = []
    for i in results:
        pkgs_list.append(i["pid"])
    return pkgs_list


def process_pkg(pkg):
    global dot
    dot += "\t" + sanitize(pkg) + " [label=\"" + pkg + "\"];\n"
    dep_ids = get_pkg_deps(get_pkg_id(pkg))
    if dep_ids is None:
        return None
    dep_pkgs = set([])
    for dep_id in dep_ids:
        dep_pkgs.add( get_pkg_name(dep_id) )
    for dep_pkg in dep_pkgs: # maybe try a while
        # dot pkg -> dep_pkg
        dot += "\t%s [label=\"%s\"];\n" % (sanitize(dep_pkg), dep_pkg)
        dot += "\t%s -> %s;\n" % ( sanitize(pkg), sanitize(dep_pkg) )
        debug("process_pkg: " + pkg + " => " + dep_pkg)
        #dep_pkgs.remove(dep_pkg)
        #process_pkg(dep_pkg) 


def process_distribution(dist_name):
    pkg_list = get_pkgs_of_dist(dist_name)
    if pkg_list is None:
        print >>sys.stderr, "No package found for distribution "+dist_name
        sys.exit(1)
    for pkg_id in pkg_list:
        process_pkg(get_pkg_name(pkg_id))


#
# M A I N
#

try:
    opts, args = getopt.getopt(sys.argv[1:], "ho:p:T:d:v", ["help", "output="])
except getopt.GetoptError, err:
    print str(err)
    usage()
    sys.exit(2)
package = None
filetype = "png"
output = "graph."+filetype
distribution = "cooker"
for o, a in opts:
    if o == "-v":
        usage()
    elif o in ("-h", "--help"):
        usage()
        sys.exit()
    elif o in ("-o", "--output"):
        output = a
    elif o == "-p":
        package = a
    elif o == "-t":
        filetype = a
    elif o == "-d":
        distribution = a
    else:
        assert False, "unhandled option"

# Database connection
try:
    conn = MySQLdb.connect('localhost','root','root', 'doc4')
except MySQLdb.Error, e:
    print "Error %d: %s" % (e.args[0], e.args[1])
    exit(1)
curs = conn.cursor(MySQLdb.cursors.DictCursor)

# generate the dot file
dot = "digraph G{\n"
if package is None:
    process_distribution(distribution)
else:
    process_pkg(package)
dot += "}"
debug(dot)

# temporally write the dot file
filename = "file.dot"
fh = open(filename, 'w')
fh.write(dot)
fh.close()

# generate the graph to output
cmd = ["/usr/bin/circo", "-T"+filetype, "-o"+str(output), filename]
try:
    retcode = subprocess.call(cmd)
    if retcode < 0:
        print >>sys.stderr, "Child was terminated by signal", -retcode
    else:
        print "Done"
except OSError, e:
    print >>sys.stderr, "Execution failed:", e

# remove the dot file
#os.remove(filename)

# Close the database
curs.close()
conn.close()
